<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <title>canvas屏幕录制</title>
    <style>
      .container {
        width: 50%;
      }
      .paint-area {
        width: 100%;
        height: 500px;
      }
      canvas {
        border: 1px solid #eee;
        margin: 0 auto;
        display: block;
        cursor: crosshair;
      }
      video {
        border: 1px solid #eee;
        display: none;
      }
    </style>
</head>
<body>
 
  <div class='container'>
    <div class="paint-area">
      <p>画板区</p>
      <canvas id='canvas-record'></canvas>
    </div>
    <button id="clearAll" onclick='clearAll()'>清除绘制</button>
    <button id="lastStep" onclick='lastStep()'>上一步</button>
    <button id="nextStep" onclick='nextStep()'>下一步</button>
    <button id="insertImage" onclick="insertImage()">插入背景图片</button>
    <div class="video-area">
      <p>视频区</p>
      <video id='video-record' playsInline autoPlay>
      </video>
    </div>
    <button id="startCapture" onclick='startCapture()'>开始录制</button>
    <button id="stopRecord" onclick='stopRecord()'>停止录制</button>
    <button id="exportVideo" onclick='exportVideo()'>导出视频</button>
  </div>

<script>
let canvas, context, video, mediaRecorder = null, stream = null
let recordeBlobs = [] // 录屏二进制文件
let canvasHistory = [] // 历史栈
let canvasStep = -1 // 当前绘制处于历史栈的哪一步
/**
 * 初始化canvas
 **/
const initCanvas = () => {
  canvas = document.querySelector('#canvas-record');
  // 设置canvas宽高
  canvas.width = document.querySelector('.paint-area').offsetWidth
  canvas.height = 400
  context = canvas.getContext('2d');
  // 下面两行必须要，不然录制视频里的背景为默认黑色，此处为修改canvas背景色
  context.fillStyle = '#fff'
  context.fillRect(0, 0, canvas.width, canvas.height)
  // 笔触样式
  context.lineWidth = 1;

  canvas.addEventListener('mousedown', startAction);
  canvas.addEventListener('mouseup', endAction);
};
const initVideo = () => {
  btnStatus('stopRecord', true)
  btnStatus('exportVideo', true)
}
/**
 * canvas涂鸦事件开始
 **/
const startAction = (e) => {
  context.strokeStyle = '#000';
  context.beginPath();
  context.moveTo(e.offsetX, e.offsetY);
  context.stroke();
  canvasHistory.push([{
    status: 'start',
    x: e.offsetX,
    y: e.offsetY
  }])
  canvasStep++
  canvas.addEventListener('mousemove', moveAction);
};
/**
 * canvas涂鸦事件过程
 **/
const moveAction = (e) => {
  context.lineTo(e.offsetX, e.offsetY);
  context.stroke();
  canvasHistory[canvasHistory.length - 1].push({
      status: 'ing',
      x: e.offsetX,
      y: e.offsetY
    })
};
/**
 * canvas涂鸦事件结束
 **/
const endAction = (e) => {
  context.closePath();
  canvasHistory[canvasHistory.length - 1].push({
    status: 'end',
    x: e.offsetX,
    y: e.offsetY
  })
  canvas.removeEventListener('mousemove', moveAction);
};
/**
 * canvas录制前置条件
 **/
const startCapture = async (e) => {
    const isSupported = isSupportedRecord()
    if (!isSupported) return
    stream = canvas.captureStream(25);
    video = document.querySelector('#video-record');
    video.width = 400;
    video.height = 300;
    video.srcObject = stream; // 屏幕实时回显
    video.style.display = 'block'
    startRecord(stream)
};
/**
 *开始录制（将流数据保存到recordeBlobs）
 **/
const startRecord = (stream) => {
  recordeBlobs = [];
  if (!mediaRecorder) {
    mediaRecorder = new MediaRecorder(stream, {
      mimeType: 'video/webm;codecs=vp8',
    });
    mediaRecorder.onstop = (e) => {
      console.log('录制完成');
    };
    mediaRecorder.ondataavailable = (e) => {
      if (e.data && e.data.size > 0) {
        recordeBlobs.push(e.data);
      }
    };
    mediaRecorder.start(100);
    btnStatus('startCapture', true)
    btnStatus('stopRecord', false)
  }
}
/**
 * 结束录制
 **/
const stopRecord = () => {
    if (!mediaRecorder) return
    mediaRecorder.stop();
    stream.getTracks().forEach((track) => track.stop());
    stream = null;
    video.style.display = 'none'
    btnStatus('stopRecord', true)
    btnStatus('exportVideo', false)
};
/**
 * 按钮禁用/解除禁用方法
 * @param id 按钮id
 * @param status 是否禁用
 */
const btnStatus = (id, status) => {
  const button = document.querySelector(`#${id}`)
  button.disabled = status
}
/**
 * 导出视频文件
 **/
const exportVideo = () => {
  if (recordeBlobs.length === 0) return
  const blob = new Blob(recordeBlobs, { type: 'video/webm;codecs=vp8' });
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  a.download = '屏幕录制.webm';
  document.body.appendChild(a);
  a.click();
  setTimeout(() => {
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
  }, 100);
}
/**
 * 判断其 MIME 格式能否被客户端录制
 **/
const isSupportedRecord = () => {
  const mimeType = 'video/webm;codecs=vp8'
  return MediaRecorder.isTypeSupported(mimeType)
}
/**
 * 清除全部
 **/
const clearAll = () => {
  canvasHistory = [] // 清除涂鸦历史栈
  context && context.clearRect(0, 0, canvas.width, canvas.height) // 清除绘制区域
}
/**
 * 涂鸦上一步
 **/
const lastStep = () => {
  canvasStep--
  if (canvasStep === -1) {
    btnStatus('lastStep', true)
  } else {
    btnStatus('lastStep', false)
  }
  btnStatus('nextStep', false)
  autoDraw(false)
}
/**
 * 根据涂鸦栈进行绘制
 **/
const autoDraw = (type) => {
  let step
  // 下一步 将笔宽重置
  if (type) {
    context.strokeStyle = '#000';
    context.lineWidth = 1
    step = canvasStep
  // 上一步 笔宽设置大一点，是为了白色能够将上一步操作覆盖掉，不然白色无法覆盖黑色轨迹
  } else {
    context.strokeStyle = '#fff';
    context.lineWidth = 2
    step = canvasStep + 1
  }
  const curStack = canvasHistory[step]
  singlePathDraw(curStack)
}
/**
 * 单次记录绘制
 * @param curStack 单次历史栈中的记录
 */
const singlePathDraw = (curStack) => {
  for (let j = 0; j < curStack.length; j++) {
    const item = curStack[j]
    if (item.status === 'start') {
      context.beginPath()
      context.moveTo(item.x, item.y)
      context.stroke()
    } else if (item.status === 'ing') {
      context.lineTo(item.x, item.y)
      context.stroke()
    } else {
      context.closePath()
    }
  }
}
/**
 * 涂鸦下一步
 **/
const nextStep = () => {
  canvasStep++
  if (canvasStep === canvasHistory.length - 1) {
    btnStatus('nextStep', true)
  } else {
    btnStatus('nextStep', false)
  }
  btnStatus('lastStep', false)
  autoDraw(true)
}
/**
 * 插入本地背景图片
 **/
const insertImage = (id) => {
  const inputObj = document.createElement('input');
  inputObj.addEventListener('change', readFile, false);
  inputObj.type = 'file';
  inputObj.accept = 'image/*';
  inputObj.id = id;
  inputObj.click();
}
/**
 * 解析本地图片并转化为base64
 **/
const readFile = function() {
  const file = this.files[0]; // 获取input输入的图片
  if(!/image\/\w+/.test(file.type)){
      alert("请确保文件为图像类型");
      return false;
  } // 判断是否图片
  const reader = new FileReader();
  reader.readAsDataURL(file); // 转化成base64数据类型
  reader.onload = function(e){
    drawLocalImageToCanvas(this.result);
  }
}
/**
 * 绘制本地图片到canvas上
 **/
const drawLocalImageToCanvas = (imgData) => {
  const img = new Image;
  img.src = imgData;
  img.onload = function() { // 必须onload之后再画
    const height = img.height * canvas.width / img.width
    context.drawImage(img, 0, 0, canvas.width, height)
    canvasHistory.map(item => {
      singlePathDraw(item)
    })
  }
}
initCanvas()
initVideo()
</script>  

</body>
</html>
